Google挖坑后人埋-ViewBinding(上)
官网镇楼
https://developer.android.com/topic/libraries/view-binding
官方警告
Warning: The 'kotlin-android-extensions' Gradle plugin is deprecated. Please use this migration guide (https://goo.gle/kotlin-android-extensions-deprecation) to start working with View Binding (https://developer.android.com/topic/libraries/view-binding) and the 'kotlin-parcelize' plugin.
kotlin-android-extensions是个好东西,可以帮我们省掉很多需要写findViewById的场景。相信大部分的Kotlin开发者都在使用它进行Android开发,而且在之前的Android Studio创建Android项目时,都会自动帮你依赖:
apply plugin: 'kotlin-android-extensions'
但是现在你再创建Android项目,就不会自动帮你依赖了,其原因就是kotlin-android-extensions这个插件已经被废弃了。
为啥?Google这新技术迭代跟玩一样啊,有kotlin-android-extensions插件我不用,我就手写,哎,就是玩儿~
其实,kotlin-android-extensions插件还是有很多问题的,虽然它很方便,但实际上,这是牺牲掉一部分内存来换取的方便,在对应用性能日益严格的今天,这种做法势必会被淘汰掉。
kotlin-android-extensions三宗罪
内存问题
通过反编译kotlin-android-extensions的代码,你就会发现,通过kotlin-android-extensions,它会在代码中创建一个HashMap,用来存放所有的id和对应的View的缓存,如果缓存中没有需要的View,那么就通过findViewById去创建,否则就直接获取,这就是它的原理。
资源ID重名
由于kotlin-android-extensions是通过view的id名直接引用的,所以多个布局间的同名id,就需要手动对import进行重命名处理,而且经常会引用错误的布局文件,导致运行崩溃。
Kotlin only
只有Kotlin才可以使用。
当然,ViewBinding也不是银弹,对比kotlin-android-extensions,它也有一些问题:
使用比kotlin-android-extensions复杂 依然有需要手动处理的场景
当然也有一些优势:
Kotlin Java通吃 空安全
ViewBinding初步
ViewBinding就是为了解决kotlin-android-extensions的这些使用问题而诞生的,它的目的只有一个,那就是避免重复的findViewById的同时,不影响应用性能。
要使用ViewBinding非常简单:
buildFeatures {
viewBinding true
}
当我们开启ViewBinding之后,在编译时,AGP会自动帮我们给每个xml布局创建一个Binding类,Binding类的命名规则是将xml文件按驼峰方式重命名后,再加上Binding作为结尾得到的,例如splash_layout.xml会自动生成一个SplashLayoutBinding的类文件。
❝跨Module使用的时候,子Module也需要开启ViewBinding功能
❞
这个Binding文件,实际上就相当于kotlin-android-extensions的HashMap,同时由于它在编译时就生成了,就不会占用运行时内存。
虽然这里生成了大量的XXXBinding文件,但是对编译速度的影响和生成Apk大小的影响几乎可以忽略:
未使用的XXXBinding文件会在混淆时被删除 编译器生成Binding文件的速度极快,同时是增加更新
ignore
如果你不想生成这个Binding类,可以通过下面的方式来过滤掉该文件的生成。
<FrameLayout
xmlns:tools="http://schemas.android.com/tools"
...
tools:viewBindingIgnore="true">
...
</FrameLayout>
使用
开启ViewBinding后,会给xml布局生成XXXBinding文件,位于build/generated/data_binding_base_class_source_out/目录下。
Activity
在Activity中使用ViewBinding一般需要使用到Binding类的inflate方法,一般使用方式如下所示。
private val binding by lazy { XxxBinding.inflate(layoutInflater) }
binding.TitleTextView.text = "Title"
Binding类还有一个getRoot方法,用来返回xml布局的根元素,所以setContentView(R.layout.xxxx)就可以替换为:
setContentView(binding.root)
Fragment
在Fragment中使用ViewBinding会比在Activity中使用要复杂一点,因为需要保证Binding类与Fragment的生命周期同步,示例代码如下所示。
class MainFragment : Fragment() {
private var _binding: OutcircleMissionFansGroupBinding? = null
private val binding get() = _binding!!
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = OutcircleMissionFansGroupBinding.inflate(inflater, container, false)
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
_binding和binding傻傻分不清吗?其实没什么区别,这是为了在Kotlin中将不可空类型置空的一种妥协方式,同样的代码逻辑,在Java中,就会非常简单了。
public class MainFragment extends Fragment {
private FragmentMainBinding binding;
@Override
public View onCreateView(@NotNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
binding = FragmentMainBinding.inflate(inflater, container, false);
return binding.getRoot();
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
}
Adapter
除了Activity和Fragment,在Adapter中使用,特别是RecyclerView中使用,也是一个非常常见的使用场景。利用kotlin-android-extensions,我们可以借助LayoutContainer来在ViewHolder中直接使用View id,那么在ViewBinding中,使用方式就更简单了。
class DemoAdapter(val dataList: List<String>) : RecyclerView.Adapter<DemoAdapter.ViewHolder>() {
inner class ViewHolder(binding: OutcircleMissionFansGroupBinding) : RecyclerView.ViewHolder(binding.root) {
val title: TextView = binding.titleTextView
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = OutcircleMissionFansGroupBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val data = dataList[position]
holder.title.text = data.title
}
override fun getItemCount() = dataList.size
}
其实核心代码依然是inflate,套路都是一样的。
Dialog
原理依然是一个套路。
override fun onCreate(savedInstanceState: Bundle?) {
binding = XXXXBinding.inflate(layoutInflater)
setContentView(binding.root)
}
Include、Merge
在布局中通过include来引入新的布局也是一个很常用的方式,kotlin-android-extensions由于底层使用的是运行时findViewById,所以不会存在什么问题,但是ViewBinding就不一样了,由于它是编译时生成,所以需要指定id才可以使用。
因此,在ViewBinding中使用include的layout,有两种方式,一种是给include设置id,这样通过id就可以直接引用,代码如下所示。
<include
android:id="@+id/xxxxx"
layout="@layout/xxxxxxx" />
这样使用的时候,代码如下:
XXXBinding.xxxInclude.xxxx
另外一种方式是直接使用新的Binding文件,因为所有的xml布局文件都会生成Binding,所以可以直接使用这个Binding文件。
IncludeXXXXXBinding.bind(binding.root).xxxxx
这种方式还可以解决Merge的引入问题。
迁移
更新一时爽,迁移火葬场。
目前还未找到现有项目从kotlin-android-extensions迁移到ViewBinding的好办法,如果当前的项目大量使用kotlin-android-extensions,那么迁移起来,就是一个巨大的工程,没有migration tools,也不能通过脚本更换,确实没找到什么好的办法。
向大家推荐下我的网站 https://xuyisheng.top/ 点击原文一键直达
专注 Android-Kotlin-Flutter 欢迎大家访问